第4章 总体设计

总体设计章节:

理解软件设计的概念与原则,了解软件总体设计任务和过程,重点难点:模块化设计原理,总体设计的过程。

了解启发式规则,掌握结构化设计方法和描绘软件结构的图形工具,重点难点:结构化设计方法、描绘软件结构的图形工具。

软件开发是从总体设计开始的,软件设计是总体设计和详细设计的总和。

总体设计包含的阶段:系统设计阶段和结构化设计阶段。

软件结构是由软件组成成分构造软件的过程、方法和表示

软件结构是由模块组成的层次系统,可以由数据流图映射而来

总体设计的任务:

  1. 寻找实现目标系统的各种不同方案,找到最佳方案
  2. 设计软件结构

好的设计关键 是模块独立。

模块的作用域在控制域之内

总体设计的基本目的就是回答“概括地说,系统应该如何实现”这个问题,因此,总体设计又称为概要设计或初步设计。

总结设计阶段的工作将划分出组成系统的物理元素——程序、文件、数据库、人工过程和文档等。

总体设计阶段的另一项重要任务是设计软件的结构,也就是要确定系统中每个程序是由哪些模块组成的,以及这些模块相互间的关系。设计出初步的软件结构后还要多方改进,从而得到更合理的结构。

总体设计过程首先寻找实现目标系统的各种不同的方案,需求分析阶段得到的数据流图是设想各种可能方案的基础,然后分析员从这些供选择的方案中选取若干个合理的方案,为每个合理的方案都准备一份系统流程图,列出组成系统的所有物理元素,进行成本/效益分析,并制定实现这个方案的进度计划。

详细设计前先进行总体设计的必要性:可以站在全局高度上,花较少成本,从较抽象的层次上分析对比多种可能的系统实现方案和软件结构,从中选出最佳方案和最合理的软件结构,从而用较低成本开发出较高质量的软件系统。

4.1设计过程

总体设计过程通常由两个主要阶段组成:

  1. 系统设计阶段,确定系统的具体实现方案
  2. 结构设计阶段,确定软件结构

典型的总体设计过程包括9个步骤:

  1. 设想供选择的方案

  2. 选取合理的方案

    对于每个合理的方案,分析员都应该准备4份资料:

    1. 系统流程图
    2. 组成系统的物理元素清单
    3. 成本/效益分析
    4. 实现这个系统的进度计划
  3. 推荐最佳的方案

  4. 功能分解

    1. 功能分解导致数据流图的进一步细化
  5. 设计软件结构

    1. 软件结构(即由模块组成的层次系统)可以用层次图或结构图来描绘
    2. 如果数据流图已经细化到适当的层次,则可以直接从数据流图映射出软件结构
  6. 设计数据库

  7. 制定测试计划

  8. 书写文档

    1. 系统说明
    2. 用户手册
    3. 测试计划
    4. 详细的实现计划
    5. 数据库设计结果
  9. 审查和复审

4.2设计原理

4.2.1模块化

模块是由边界元素限定的相邻程序元素的序列,而且有一个总体标识符代表它。

模块化就是把程序划分成独立命名且可独立访问的模块,每个模块完成一个子功能,把这些模块集成起来构成一个整体,可以完成指定的功能满足用户的需求。

模块化是为了使一个复杂的大型程序能被人的智力所管理,是软件应该具备的唯一属性。如果一个大型程序仅由一个模块组成,它将很难被人所理解。

把复杂的问题分解成许多容易解决的小问题,原来的问题也就容易解决了。这就是模块化的根据。

每个程序都相应地有一个最适当的模块数目M,使得系统的开发成本最小。

评价一种设计方法定义模块能力的五条标准:

  1. 模块可分解性

  2. 模块可组装性

  3. 模块可理解性

  4. 模块连续性

  5. 模块保护性

作用:采用模块化原理可以使软件结构清晰,不仅容易设计也容易阅读和理解。因为程序错误通常局限在有关的模块及它们之间的接口中,所以模块化使软件容易测试和调试,因而有助于提高软件的可靠性。因为变动往往只涉及少数几个模块,所以模块化能够提高软件的可修改性。模块化也有助于软件开发工程的组织管理,一个复杂的大型程序可以由许多程序员分工编写不同的模块,并且可以进一步分配技术熟练的程序员编写困难的模块。

4.2.2抽象

现实世界中一定事物、状态或过程之间总存在着某些相似的方面(共性)。把这些相似的方面集中和概括起来,暂时忽略它们之间的差异,这就是抽象。

抽象就是抽出事物本质特性而暂时不考虑细节。

人类在认识复杂现象的过程中使用的最强有力的思维工具是抽象。

一般抽象过程:

处理复杂系统的惟一有效的方法是用层次的方式构造和分析它。

一个复杂的动态系统首先可以用一些高级的抽象概念构造和理解,这些高级概念又可以用一些较低级的概念构造和理解,如此进行下去,直至最低层次的具体元素。

软件工程抽象过程:

软件工程过程的每一步都是对软件解法的抽象层次的一次精化。

  1. 在可行性研究阶段,软件作为系统的一个完整部件;
  2. 在需求分析期间,软件解法是使用在问题环境内熟悉的方式描述的;
  3. 当由总体设计向详细设计过渡时,抽象的程度也就随之减少了;
  4. 最后,当源程序写出来以后,也就达到了抽象的最低层。

逐步求精和模块化的概念,与抽象是紧密相关的。随着软件开发工程的进展,在软件结构每一层中的模块,表示了对软件抽象层次的一次精化。事实上,软件结构顶层的模块,控制了系统的主要功能并且影响全局;在软件结构底层的模块,完成对数据的一个具体处理,用自顶向下由抽象到具体的方式分配控制,简化了软件的设计和实现,提高了软件的可理解性和可测试性,并且使软件更容易维护。

4.2.3逐步求精

逐步求精是人类解决复杂问题时采用的基本方法,也是许多软件工程技术(例如,规格说明技术,设计和实现技术)的基础。

可以把逐步求精定义为:为了能集中精力解决主要问题而尽量推迟对问题细节的考虑。

Miller法则:一个人在任何时候都只能把注意力集中在(7±2)个知识块上。

逐步求精最初是由Niklaus Wirth提出的一种自顶向下的设计策略。按照这种设计策略,程序的体系结构是通过逐步精化处理过程的层次而设计出来的。通过逐步分解对功能的宏观陈述而开发出层次结构,直至最终得出用程序设计语言表达的程序。

求精实际上是细化过程。求精要求设计者细化原始陈述。

抽象与求精是一对互补的概念。抽象使得设计者能够说明过程和数据,同时却忽略低层细节。事实上,可以把抽象看作是一种通过忽略多余的细节同时强调有关的细节,而实现逐步求精的方法。求精则帮助设计者在设计过程中逐步揭示出低层细节。这两个概念都有助于设计者在设计演化过程中创造出完整的设计模型。

4.2.4信息隐藏和局部化

**信息隐藏:**应该这样设计和确定模块,使得一个模块内包含的信息(过程和数据)对于不需要这些信息的模块来说,是不能访问的。

**局部化:**局部化的概念和信息隐藏概念是密切相关的。所谓局部化是指把一些关系密切的软件元素物理地放得彼此靠近。显然,局部化有助于实现信息隐藏。

实际上,应该隐藏的不是有关模块的一切信息,而是模块的实现细节。

信息隐藏和局部化的作用:

  1. “隐藏”意味着有效的模块化可以通过定义一组独立的模块而实现,这些独立的模块彼此间仅仅交换那些为了完成系统功能而必须交换的信息。
  2. 使用信息隐藏原理作为模块化系统设计的标准就会带来极大好处。因为绝大多数数据和过程对于软件的其他部分而言是隐藏的,在修改期间由于疏忽而引入的错误就很少可能传播到软件的其他部分。

4.2.5模块独立

模块独立的概念是模块化、抽象、信息隐藏和局部化概念的直接结果。

开发具有独立功能而且和其他模块之间没有过多的相互作用的模块,就可以做到模块独立。换句话说,希望这样设计软件结构,使得每个模块完成一个相对独立的特定子功能,并且和其他模块之间的关系很简单。

模块独立的重要性:(模块独立是好设计的关键,而设计又是决定软件质量的关键环节)

  1. 有效的模块化(即具有独立的模块)的软件比较容易开发出来。这是由于能够分割功能而且接口可以简化,当许多人分工合作开发同一个软件时,这个优点尤其重要。
  2. 独立的模块比较容易测试和维护。这是因为相对说来,修改设计和程序需要的工作量比较小,错误传播范围小,需要扩充功能时能够“插入”模块。

模块独立程度的两个定性标准度量:

耦合衡量不同模块彼此间互相依赖(连接)的紧密程度。耦合要低,即每个模块和其他模块之间的关系要简单;

内聚衡量一个模块内部各个元素彼此结合的紧密程度。内聚要高,每个模块完成一个相对独立的特定子功能。

1.耦合

耦合是对一个软件结构内不同模块之间互连程度的度量。耦合强弱取决于模块间接口的复杂程度,进入或访问一个模块的点,以及通过接口的数据。

在软件设计中应该追求尽可能松散耦合的系统。在这样的系统中:

  1. 可以研究、测试或维护任何一个模块,而不需要对系统的其他模块有很多了解;
  2. 模块间联系简单,发生在一处的错误传播到整个系统的可能性就很小;
  3. 模块间的耦合程度强烈影响系统的可理解性、可测试性、可靠性和可维护性。

耦合程度的度量:

  1. 非直接耦合/完全独立:如果两个模块中的每一个都能独立地工作而不需要另一个模块的存在,那么它们完全独立。在一个软件系统中不可能所有模块之间都没有任何连接。

  2. 数据耦合:低耦合,如果两个模块彼此间通过参数交换信息,而且交换的信息仅仅是数据,那么这种耦合称为数据耦合。

    评价:

    • 系统中至少必须存在这种耦合。一般说来,一个系统内可以只包含数据耦合。

    • 数据耦合是理想的目标。

    • 维护更容易,对一个模块的修改不会是另一个模块产生退化错误。

  3. 控制耦合:中等程度的耦合,如果两个模块彼此间传递的信息中有控制信息,这种耦合称为控制耦合。

    评价:

    • 控制耦合往往是多余的,把模块适当分解之后通常可以用数据耦合代替它。

    • 被调用的模块需知道调用模块的内部结构和逻辑,降低了重用的可能性 。

  4. 特征耦合:当把整个数据结构作为参数传递而被调用的模块只需要使用其中一部分数据元素时,就出现了特征耦合。

    评价:

    • 被调用的模块可使用的数据多于它确实需要的数据,这将导致对数据的访问失去控制,从而给计算机犯罪提供了机会。

    • 无论何时把指针作为参数进行传递,都应该仔细检查该耦合。

  5. 公共环境耦合:当两个或多个模块通过一个公共数据环境相互作用时,它们之间的耦合称为公共环境耦合。公共环境可以是全程变量、共享的通信区、内存的公共覆盖区、任何存储介质上的文件、物理设备等等。

    公共环境耦合的复杂程度随耦合的模块个数而变化,当耦合的模块个数增加时复杂程度显著增加。
    
    如果只有两个模块有公共环境,则这种耦合有两种可能:
    
    1. 一个模块往公共环境送数据,另一个模块从公共环境取数据。数据耦合的一种形式,是比较松散的耦合。
    
    2. 两个模块都既往公共环境送数据又从里面取数据,这种耦合比较紧密,介于数据耦合和控制耦合之间。
    
  6. 内容耦合:最高程度的耦合,应该避免使用。

    如果出现下列情况之一,两个模块间就发生了内容耦合:

    • 一个模块访问另一个模块的内部数据;
    • 一个模块不通过正常入口转到另一个模块的内部;
    • 两个模块有一部分程序代码重叠;
    • 一个模块有多个入口。

总结: 耦合是影响软件复杂程度的一个重要因素。应该采取下述设计原则:

  1. 尽量使用数据耦合。

  2. 少用控制耦合和特征耦合。

  3. 限制公共环境耦合的范围。

  4. 完全不用内容耦合。

2.内聚

内聚:标志一个模块内各个元素彼此结合的紧密程度,它是信息隐藏和局部化概念的自然扩展。简单地说,理想内聚的模块只做一件事情。

设计时应该力求做到高内聚,通常中等程度的内聚也是可以采用的,而且效果和高内聚相差不多;但是,低内聚不要使用。

内聚和耦合是密切相关的,模块内的高内聚往往意味着模块间的松耦合。实践表明内聚更重要,应该把更多注意力集中到提高模块的内聚程度上。

内聚程度的度量:

  1. 偶然内聚:如果一个模块完成一组任务,这些任务彼此间即使有关系,关系也是很松散的,就叫做偶然内聚。

    评价:

    • 模块内各元素之间没有实质性联系,很可能在一种应用场合需要修改这个模块,在另一种应用场合又不允许这种修改,从而陷入困境;
    • 可理解性差,可维护性产生退化;
    • 模块是不可重用的。

    解决方案:将模块分成更小的模块,每个小模块执行一个操作。

  2. 逻辑内聚:如果一个模块完成的任务在逻辑上属于相同或相似的一类,则称为逻辑内聚。

    评价:

    • 接口难以理解,造成整体上不易理解;
    • 完成多个操作的代码互相纠缠在一起,即使局部功能的修改有时也会影响全局,导致严重的维护问题;

    解决方案:模块分解。

  3. 时间内聚:如果一个模块包含的任务必须在同一段时间内执行,就叫时间内聚。

    评价:

    • 时间关系在一定程度上反映了程序某些实质,所以时间内聚比逻辑内聚好一些。
    • 模块内操作之间的关系很弱,与其他模块的操作却有很强的关联。
    • 时间内聚的模块不太可能重用。
  4. 过程内聚:如果一个模块内的处理元素是相关的,而且必须以特定次序执行,则称为过程内聚。使用程序流程图作为工具设计软件时,常常通过研究流程图确定模块的划分,这样得到的往往是过程内聚的模块。

    评价:

    • 比时间内聚好,至少操作之间是过程关联的。
    • 仍是弱连接,不太可能重用模块。

    解决方案:分割为单独的模块,每个模块执行一个操作。

  5. 通信内聚:如果模块中所有元素都使用同一个输入数据和(或)产生同一个输出数据,则称为通信内聚。即在同一个数据结构上操作。

    评价:

    • 模块中各操作紧密相连,比过程内聚更好。
    • 不能重用。

    解决方案:分成多个模块,每个模块执行一个操作。

  6. 顺序内聚:如果一个模块内的处理元素和同一个功能密切相关,而且这些处理必须顺序执行,则称为顺序内聚。

    评价:根据数据流图划分模块时,通常得到顺序内聚的模块,这种模块彼此间的连接往往比较简单。

  7. 功能内聚:如果模块内所有处理元素属于一个整体,完成一个单一的功能,则称为功能内聚。功能内聚是最高程度的内聚。

    评价:

    • 模块可重用,应尽可能重用;
    • 可隔离错误,维护更容易;
    • 扩充产品功能时更容易。

七种内聚的优劣评分结果

类型名称评分
高内聚功能内聚10分
顺序内聚9分
中内聚通信内聚7分
过程内聚5分
低内聚时间内聚3分
逻辑内聚1分
偶然内聚0分

设计时力争做到高内聚,并且能够辨认出低内聚的模块。

4.3启发规则

1. 改进软件结构提高模块独立性

通过模块分解或合并,降低耦合提高内聚。

两个方面:

  1. 模块功能完善化。一个完整的模块包含:

    • 执行规定的功能的部分

    • 出错处理的部分

    • 返回一个“结束标志”

  2. 消除重复功能,改善软件结构。

    • 完全相似
    • 局部相似

2. 模块规模应该适中

经验表明,一个模块的规模不应过大,最好能写在一页纸内。通常规定50~100行语句,最多不超过500行。数字只能作为参考,根本问题是要保证模块的独立性。

过大的模块往往是由于分解不充分,但是进一步分解必须符合问题结构,一般说来,分解后不应该降低模块独立性。

过小的模块开销大于有效操作,而且模块数目过多将使系统接口复杂。

3. 深度、宽度、扇出和扇入都应适当

**深度:**软件结构中控制的层数,它往往能粗略地标志一个系统的大小和复杂程度。

**宽度:**软件结构内同一个层次上的模块总数的最大值。

**扇出:**一个模块直接控制(调用)的模块数目。

**扇入:**有多少个上级模块直接调用它。

4. 模块的作用域应该在控制域之内

**模块的作用域:**定义为受该模块内一个判定影响的所有模块的集合。

**模块的控制域:**是这个模块本身以及所有直接或间接从属于它的模块的集合。

在一个设计得很好的系统中,所有受判定影响的模块应该都从属于做出判定的那个模块,最好局限于做出判定的那个模块本身及它的直属下级模块。

违反规定的解决方案:

  1. 把做判定的点往上移
  2. 把那些在作用域但不在控制域内的模块移到控制域内

4. 力争降低模块接口的复杂度

模块接口复杂是软件发生错误的一个主要原因。应该仔细设计模块接口,使得信息传递简单并且和模块的功能一致。

接口复杂或不一致(即看起来传递的数据之间没有联系),是紧耦合或低内聚的征兆,应该重新分析这个模块的独立性。

6. 设计单入口单出口的模块

警告软件工程师不要使模块间出现内容耦合。当从顶部进入模块并且从底部退出来时,软件是比较容易理解的,因此也是比较容易维护的。

7. 模块功能应该可以预测

模块的功能应该能够预测,但也要防止模块功能过分局限。

功能可预测:如果一个模块可以当做一个黑盒子,只要输入的数据相同就产生同样的输出,这个模块的功能就是可以预测的。

4.4 描绘软件结构的图形工具

4.4.1 层次图和HIPO图

  1. 层次图(H图):层次图用来描绘软件的层次结构。很适于在自顶向下设计软件的过程中使用。
层次图层次方框图
作用描绘软件结构描绘数据结构
矩形框模块数据元素
连线调用关系组成关系
  1. HIPO图:HIPO图是美国IBM公司发明的“层次图+输入/处理/输出图”的英文缩写。为了能使HIPO图具有可追踪性,在H图(层次图)里除了最顶层的方框之外,每个方框都加了编号。

    和H图中每个方框相对应,应该有一张IPO图描绘这个方框代表的模块的处理过程。模块在H图中的编号便于追踪了解这个模块在软件结构中的位置。

4.4.2 结构图

Yourdon提出的结构图是进行软件结构设计的另一个有力工具。结构图和层次图类似,也是描绘软件结构的图形工具。

基本符号:

  1. 方框代表一个模块
  2. 方框之间的直线表示模块的调用关系
  3. 尾部是空心圆箭头表示传递的是数据
  4. 尾部是实心圆箭头表示传递的是控制信息

img

附加符号:

  1. 选择调用:判定为真时调用A,为假时调用B
  2. 循环调用:模块M循环调用模块A、B、C

img

注意:

  1. 层次图和结构图并不严格表示模块的调用次序,多数人习惯按调用次序从左到右画模块;
  2. 层次图和结构图并不指明何时调用下层模块;
  3. 层次图和结构图只表明一个模块调用那些模块,没有表示模块内还有没有其他成分;
  4. 通常用层次图作为描绘软件结构的文档;、
  5. 由层次图导出结构图的过程,可以作为检查设计正确性和评价模块独立性的好方法。

4.5 面向数据流的设计方法

面向数据流的设计方法的目标,是给出设计软件结构的一个系统化的途径。面向数据流的设计方法定义了一些“映射”,利用这些映射可以把数据流图变换成软件结构。

因为任何软件系统都可以用数据流图表示,所以面向数据流的设计方法理论上可以设计任何软件的结构。通常所说的结构化设计方法(简称SD方法),也就是基于数据流的设计方法。

4.5.1 概念

面向数据流的设计方法把信息流映射成软件结构,信息流的类型决定了映射的方法。

信息流有两种类型:

  1. 变换流:信息沿输入通路进入系统,同时由外部形式变换成内部形式,进入系统的信息通过变换中心,经加工处理以后再沿输出通路变换成外部形式离开软件系统。

    img

  2. 事务流:数据沿输入通路到达一个处理T,T根据输入数据的类型在若干个动作序列中选出一个来执行。

    处理T称为事务中心,它完成下述任务:

    • 接收输入数据
    • 分析每个事务以确定它的类型
    • 根据事务类型选取一条活动通路

    img

面向数据流方法设计过程:

img

4.5.2变换分析

变换分析是一系列设计步骤的总称,经过这些步骤把具有变换流特点的数据流图按预先确定的模式映射成软件结构。

设计步骤:

  1. 复查基本系统模型

  2. 复查并精化数据流图

  3. 确定数据流图具有变换特性还是事务特性

  4. 确定输入流和输出流的边界,从而孤立出变换中心

  5. 完成“第一级分解”(软件结构代表对控制的自顶向下的分配,所谓分解就是分配控制的过程。)

    位于软件结构最顶层的控制模块Cm协调下述从属的控制功能:

    • 输入信息处理控制模块Ca,协调对所有输入数据的接收;
    • 变换中心控制模块Ct,管理对内部形式的数据的所有操作;
    • 输出信息处理控制模块Ce,协调输出信息的产生过程。
  6. 完成“第二级分解”

    处理映射成软件结构中一个适当的模块。完成第二级分解的方法是:

    • 从变换中心的边界开始沿着输入通路向外移动,把输入通路中每个处理映射成软件结构中Ca控制下的一个低层模块;

    • 然后沿输出通路向外移动,把输出通路中每个处理映射成直接或间接受模块Ce控制的一个低层模块;

    • 最后把变换中心内的每个处理映射成受Ct控制的一个模块。

  7. 使用设计度量和启发式规则对第一次分割得到的软件结构进一步精化

4.5.3 事务分析

虽然在任何情况下都可以使用变换分析方法设计软件结构,但是在数据流具有明显的事务特点时,也就是有一个明显的“发射中心”(事务中心)时,还是以采用事务分析方法为宜。

事务分析的设计步骤和变换分析的设计步骤大部分相同或类似,主要差别仅在于由数据流图到软件结构的映射方法不同:

  • 由事务流映射成的软件结构包括一个接收分支和一个发送分支;

  • 映射出接收分支结构的方法和变换分析映射出输入结构的方法很相像,即从事务中心的边界开始,把沿着接收流通路的处理映射成模块;

  • 发送分支的结构包含一个调度模块,它控制下层的所有活动模块;然后把数据流图中的每个活动流通路映射成与它的流特征相对应的结构。

总结:

一般说来,如果数据流不具有显著的事务特点,最好使用变换分析;反之,如果具有明显的事务中心,则应该采用事务分析技术。

机械地遵循变换分析或事务分析的映射规则,可能会得到一些不必要的控制模块,如果它们确实用处不大,那么可以而且应该把它们合并。

如果一个控制模块功能过分复杂,则应该分解为两个或多个控制模块,或者增加中间层次的控制模块。

4.5.4 设计优化

设计优化应该力求做到在有效的模块化的前提下使用最少量的模块,以及在能够满足信息要求的前提下使用最简单的数据结构。

对于时间是决定性因素的应用场合,可能有必要在详细设计阶段,也可能在编写程序的过程中进行优化。

软件开发人员应该认识到,程序中相对说比较小的部分(典型地,10%~20%),通常占用全部处理时间的大部分(50%~80%)。

对时间起决定性作用的软件进行优化:

  • 在不考虑时间因素的前提下开发并精化软件结构;
  • 在详细设计阶段选出最耗费时间的那些模块,仔细地设计它们的处理过程,以求提高效率;
  • 使用高级程序设计语言编写程序;
  • 在软件中孤立出那些大量占用处理机资源的模块;
  • 必要时重新设计或用依赖于机器的语言重写上述大量占用资源的模块的代码,以求提高效率。